winbrew_app\operations\install/
plan.rs

1use super::Result;
2use super::state;
3use super::{InstallObserver, ResolvedInstallTarget, resolve_install_target, sevenz};
4use crate::models::domains::package::PackageRef;
5use crate::models::domains::shared::DeploymentKind;
6use url::Url;
7
8/// A read-only install preview.
9pub struct InstallPreview {
10    target: ResolvedInstallTarget,
11    inspection: state::InstallTargetInspection,
12    ignore_checksum_security: bool,
13}
14
15/// Build a read-only preview of the install operation.
16pub fn build_install_preview<O: InstallObserver>(
17    ctx: &crate::AppContext,
18    package_ref: PackageRef,
19    ignore_checksum_security: bool,
20    observer: &mut O,
21) -> Result<InstallPreview> {
22    let target = resolve_install_target(ctx, package_ref, |query, matches| {
23        observer.choose_package(query, matches)
24    })?;
25    let conn = crate::database::get_conn()?;
26    let inspection = state::inspect_install_target_with_commands(
27        &conn,
28        &target.package.name,
29        &target.install_dir,
30        target.resolved_commands_json.as_deref(),
31    )?;
32
33    Ok(InstallPreview {
34        target,
35        inspection,
36        ignore_checksum_security,
37    })
38}
39
40/// Return the human-readable lines that describe the preview.
41pub fn preview_lines(
42    ctx: &crate::AppContext,
43    preview: &InstallPreview,
44    show_temp_root: bool,
45) -> Vec<String> {
46    let mut lines = Vec::new();
47
48    lines.push(format!(
49        "Package: {} {}",
50        preview.target.package.name, preview.target.package.version
51    ));
52    lines.push(format!(
53        "Installer URL: {}",
54        shorten_url(&preview.target.installer.url)
55    ));
56    lines.push(format!(
57        "Download payload: {}",
58        match preview
59            .target
60            .download_path
61            .file_name()
62            .and_then(|value| value.to_str())
63        {
64            Some(file_name) => file_name.to_string(),
65            None => preview.target.download_path.display().to_string(),
66        }
67    ));
68    let engine = preview.target.manifest_engine.as_str();
69    let deployment_kind = preview.target.manifest_deployment_kind.as_str();
70    lines.push(format!("Engine: {engine}"));
71    if preview.target.manifest_deployment_kind
72        != default_deployment_kind_for_engine(preview.target.manifest_engine)
73    {
74        lines.push(format!("Deployment: {deployment_kind}"));
75    }
76    lines.push(format!(
77        "Install dir: {}",
78        preview.target.install_dir.display()
79    ));
80    if show_temp_root {
81        lines.push(format!("Temp root: {}", preview.target.temp_root.display()));
82    }
83    lines.push(format!(
84        "Checksum policy: {}",
85        if preview.ignore_checksum_security {
86            "legacy algorithms allowed"
87        } else {
88            "strict"
89        }
90    ));
91
92    match preview.target.resolved_commands.as_deref() {
93        Some(commands) if !commands.is_empty() => {
94            lines.push(format!("Command shims: {}", commands.join(", ")));
95        }
96        _ => {}
97    }
98
99    if preview.target.runtime_bootstrap_required {
100        lines.push(format!(
101            "7-Zip runtime bootstrap: required for {}",
102            sevenz::sevenz_runtime_dir_from_runtime_root(&ctx.paths.root).display()
103        ));
104    } else {
105        lines.push("7-Zip runtime bootstrap: not required".to_string());
106    }
107
108    match preview.inspection.state {
109        state::InstallTargetState::Ready => {
110            lines.push("Preflight: no blockers found".to_string());
111        }
112        state::InstallTargetState::AlreadyInstalled => {
113            lines.push("Preflight blocker: package is already installed".to_string());
114        }
115        state::InstallTargetState::AlreadyInstalling => {
116            lines.push("Preflight blocker: package is already installing".to_string());
117        }
118        state::InstallTargetState::CurrentlyUpdating => {
119            lines.push("Preflight blocker: package is currently updating".to_string());
120        }
121        state::InstallTargetState::Failed => {
122            lines.push("Preflight: stale failed record will be cleaned up".to_string());
123        }
124        state::InstallTargetState::Orphaned => {
125            lines.push("Preflight: orphaned install directory will be cleaned up".to_string());
126        }
127    }
128
129    for conflict in &preview.inspection.command_conflicts {
130        lines.push(format!(
131            "Preflight blocker: command '{}' is already exposed by package '{}'",
132            conflict.command, conflict.package
133        ));
134    }
135
136    lines
137}
138
139fn shorten_url(raw_url: &str) -> String {
140    let Ok(parsed_url) = Url::parse(raw_url) else {
141        return raw_url.to_string();
142    };
143
144    let Some(host) = parsed_url.host_str() else {
145        return raw_url.to_string();
146    };
147
148    let segments = parsed_url
149        .path_segments()
150        .map(|segments| {
151            segments
152                .filter(|segment| !segment.is_empty())
153                .collect::<Vec<_>>()
154        })
155        .unwrap_or_default();
156
157    match segments.as_slice() {
158        [] => host.to_string(),
159        [only] => format!("{host}/{only}"),
160        [.., last] => format!("{host}/.../{last}"),
161    }
162}
163
164fn default_deployment_kind_for_engine(engine: crate::engines::EngineKind) -> DeploymentKind {
165    match engine {
166        crate::engines::EngineKind::Msix
167        | crate::engines::EngineKind::Msi
168        | crate::engines::EngineKind::NativeExe
169        | crate::engines::EngineKind::Font => DeploymentKind::Installed,
170        crate::engines::EngineKind::Zip | crate::engines::EngineKind::Portable => {
171            DeploymentKind::Portable
172        }
173    }
174}